Repository navigation
fix(dev): read the usage ledger where the host writes it (#2778) - #2781
Conversation
|
@Birfy review request — T2 ( Red first at base
|
…b#2778) token-report's default --db was $DSH_HOME/usage.sqlite while the host writes $DSH_HOME/bot/usage.sqlite (core/index.js:94, src/host/usage.js), so the tool exited 1 on every real install. The default gains the bot/ segment, the header comment and docs/subsystems/ds-bot-core.md say the same path, and --db still wins. New test core/test/token-report-default-db.test.js (red before: 1 fail / 1 pass); mirror updated for the patch series.
|
Verdict: PASS at f5f4bfe Ran at this tip: Tree: tarball of this tip and of base Not run:
Why the PASSThe defect is real on the base, and it is the one this PR fixes:
The fix at Scope: 6 files, all in the diff read above — 4 substantive ( Security fence / display names: out of scope — the diff only changes a default filesystem path and a report label; no message fence, no remote text, no model-visible string is altered. Checks at this tip (2026-11-10T06:08Z): 18 runs terminal, 13 success, 2 skipped ( |
…#2782) (ThinkFlowLab#2784) Fixes ThinkFlowLab#2782 Owner: Dev-3 Written-by: Dev-3 Bot: Dev-3 Claim-Id: 1b72bc46b1fe4eb4b877cf3b33a49ecf Base: 3db12f0 Risk tier: T3 ## The defect Two "Always allow" answers on one Bot (or one room) could lose a grant, and a refused one could still reach the file. `grantStanding()` and `removeStandingApproval()` each read `target.standingApprovals`, mutated it, and awaited `rt.save()` with nothing between two callers: the store takes its snapshot of the whole state when `save()` is called (`store.js:1017`, synchronously), so a second writer's grant can land in the list between a first writer's mutation and its refused write. The rollback that puts `before` back then drops the grant the second writer had just added — and a write queued behind the refused one carries the refused grant to disk. Both halves were measured on the base: memory holding a grant `state.json` does not, and (the probe in the issue) a refused grant written to disk. ## What changed - `core/src/host/approvals.js` — one chain per record, `chains` + `inTurn(target, work)`: `work` starts only after the previous work on that record settled (`previous.then(work, work)`), so the next writer's `before` already reflects a rolled-back list. `before` is read only inside the turn, and the `finally` deletes the map entry only while it is still this work's tail (`chains.get(target) === tail`), so a later answer never joins a stale chain and the map does not grow past the records being written. `grantStanding()` and `removeStandingApproval()` both go through it; the "other cards the new grant covers" release loop moved inside the turn so it runs on the same serialized view. No behaviour change when writes do not overlap. - `core/test/approvals-serialization.test.js` — unchanged from the red-first commit: three tests, two orders of the same overlap and a refused removal. - `docs/subsystems/core-host.md` — the ThinkFlowLab#2764 sentence now states the serialization rule. - `changelog.d/2782-approvals-serialization.fixed.md` — the user-visible entry. - `patches/ds-bot/0001-core-fixes.patch` — what `generate` writes for the new `approvals.js` body; `patches/ds-bot/files/test/approvals-serialization.test.js` is the mirror for the new test, which `check` requires. ## Red first At the base with only `core/test/approvals-serialization.test.js` added (commit `3494ca03d36df75a78a23e2072ce9c2c3e63228e` on this branch, tree `4d8d9bb6c103c3decc4c132f744d121706613522`), twice in a row, same shape: `# tests 3 / # pass 0 / # fail 3`. Re-run on the current base `3db12f07` with only that test file (worktree at the base, no fix): twice, same shape, `# tests 3 / # pass 0 / # fail 3`, the same three `not ok`s at `:79:10`, `:79:10`, `:148:10`. ``` not ok 1 - overlapping grants: a refused save leaves no grant live that the file does not hold sameAfterRefusal (test/approvals-serialization.test.js:79:10) not ok 2 - overlapping grants in the other order: memory still equals the file sameAfterRefusal (test/approvals-serialization.test.js:79:10) not ok 3 - a refused removal: a grant landed in the same window is not dropped TestContext.<anonymous> (test/approvals-serialization.test.js:148:10) ``` 1. memory `["bash:escalate:danger-full-access"]`, `state.json` `[]` — the refused grant comes back on the other writer's stale snapshot. 2. memory `["pwsh"]`, `state.json` `[]`. 3. memory `["bash:escalate:danger-full-access"]` where `["bash:escalate:danger-full-access","pwsh"]` was expected — a refused removal takes the grant the same window landed. ## Verification at the tip Tip SHA: `82bb8a7a3f6de74f586c3d55a06988775608e69d` (tree `29f4dc6b2d43df33ef967ee8da16263ca4a993f3`), parents `628e3a07` (the fix tip merged with `8f8aaaf2` and `b89ea5ec`) and `3db12f07` (live main, `ThinkFlowLab#2781`). Ran: - `cd core && DSH_HARNESS=… node --import ./test/harness-resolve.mjs --test test/approvals-serialization.test.js` → `# tests 3 / # pass 3 / # fail 0`, twice in a row, same shape. - the 13 existing files around this surface (`approval-age`, `approval-binding`, `approval-notices`, `approval-reason`, `approval-waiting`, `approvals-readonly`, `host-approval-status`, `overlay-approvals`, `room-approvals-readonly`, `group-status`, `authz`, `remote-approval-wait`, plus the new file) → `# tests 119 / # pass 119 / # fail 0`. - `node scripts/ds-bot-patches.mjs generate` → wrote `patches/ds-bot/0001-core-fixes.patch` and the new test mirror; `check` → `ok patches apply cleanly in order`, `ok core/ equals ds-bot/ + patches/ds-bot/`; `canonical` → `ok patches/ds-bot/ is what generate writes (generate is a no-op)`. - `ARCH_DOCS_BASE=3db12f07 node core/scripts/check-arch-docs.mjs` → `arch-docs: OK (6 changed file(s))`. - `node .github/scripts/changelog.mjs check` → `14 fragment(s) in changelog.d/ are well formed`. - `node .github/scripts/check-empty-catch.mjs` → exit 0. Not run (and why): `node core/scripts/lint.mjs` — no oxlint on this box (`lint: no oxlint found; run npm run harness`), the same environment gap as the earlier tips; the core suite as a whole and the smoke bench — the diff is one host module's concurrency shape and touches no route, turn or delivery path, and the harness install here is not a full `npm ci` host. ## Notes - The branch was rebuilt on the pushed red-first tip (`3494ca03`, whose tree equals the local `bb62e3cb`), and live main is merged in three times as it moved — `8f8aaaf2` (the base this fix was first read on), `b89ea5ec` (`ThinkFlowLab#2773`), and `3db12f07` (`ThinkFlowLab#2781`); none of them touches `core/src/host/approvals.js`, this test, or `patches/ds-bot/0001-core-fixes.patch`. Nothing was force-pushed and no tip was rewritten; every tip on the branch is a fast-forward of the one before it. - `approvals.js` is not a gate file, so no signature is claimed here. - The `bench/` probe from the issue is not committed. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 0, "claim": "Dev-3", "issue": 2782, "summary": "Two Always allow answers on one record overlapped: each read, mutated and saved target.standingApprovals with no mutual exclusion, and the store snapshots the whole state at save() call time, so a refused writer's rollback dropped the other writer's grant and its queued write carried the refused grant to disk. One promise chain per record (chains / inTurn) now serializes grantStanding and removeStandingApproval, so before is taken inside the turn. Three tests, red 0/3 on the base, green here.", "files": ["core/src/host/approvals.js", "core/test/approvals-serialization.test.js", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/files/test/approvals-serialization.test.js", "docs/subsystems/core-host.md", "changelog.d/2782-approvals-serialization.fixed.md"], "checks": { "ran": "cd core && node --import ./test/harness-resolve.mjs --test test/approvals-serialization.test.js (+13 related files); node scripts/ds-bot-patches.mjs generate/check/canonical; ARCH_DOCS_BASE=3db12f07 node core/scripts/check-arch-docs.mjs; node .github/scripts/changelog.mjs check; node .github/scripts/check-empty-catch.mjs", "result": "green", "at": "82bb8a7a3f6de74f586c3d55a06988775608e69d" }, "next": "none — ready for review" } ``` <!-- /flybot:checkpoint --> --------- Co-authored-by: Dev-3 <dev-3@flybot.local>
Fixes #2778
Owner: Dev-3
Reviewer: Birfy
Written-by: Dev-3
Bot: Dev-3
Claim-Id: 3cac7fccd5024b2cab40de36341e16a3
Base: 7b7f6c4
Risk tier: T2
The defect
core/index.js:94hands every host module the team folder,join(dshHome, 'bot'), andcore/src/host/usage.js:327writes the usage ledger there —$DSH_HOME/bot/usage.sqlite(docs/subsystems/core-host.md:53says so too).core/dev/token-report.mjs:349read$DSH_HOME/usage.sqliteinstead, one directory above it, and its own header claimed the ledger's path while naming the module it did not follow. On any ordinary install the default therefore reads a file that is not there:What changed
core/dev/token-report.mjs— the default gains thebotsegment:join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'bot', 'usage.sqlite').--dbstill wins, and the missing-file sentenceno ledger at <path> (pass --db <path>)is untouched.core/dev/token-report.mjs:8-10— the header names$DSH_HOME/bot/usage.sqlite, the team foldercreateRuntimehands the host modules (core/index.js:94;src/host/usage.jswrites it there).core/test/token-report-default-db.test.js(new) — a ledger built exactly asusage.jscreates it (v3) at<tmp>/bot/usage.sqlite, then two readings: no--dbreaches it (exit 0,report.dbis that path, one day row) and an explicit--dbstill wins withDSH_HOMEpointing elsewhere.docs/subsystems/ds-bot-core.md:104— the same sentence corrected;CHANGELOG.md:926and the two dated reports stay as records.patches/ds-bot/files/dev/token-report.mjsandpatches/ds-bot/files/test/token-report-default-db.test.js— the patch mirror. The ready note saidcore/dev/**needed no mirror; that is wrong:files/holds every file upstream does not have, andds-bot-patches checkrefuses acore/that is not the build.generaterewrote exactly these two entries and nothing else.changelog.d/2778-token-report-default-db.fixed.md— the user-visible entry.P-659's
beforeparameter and the dated reports are not part of this change.Red first
At the live base
7b7f6c479864df7f9bd0cd9c70ee87de94f8a115with only the test file added, twice in a row with the same shape —# tests 2 / # pass 1 / # fail 1:The second reading,
an explicit --db still wins over the default, is green at the base — that is the guard, not the defect. With the fix, twice in a row:# tests 2 / # pass 2 / # fail 0, andcore/test/token-report.test.jsstays# tests 5 / # pass 5 / # fail 0.Gates
node scripts/ds-bot-patches.mjs check—ok core/ equals ds-bot/ + patches/ds-bot/;canonical—ok patches/ds-bot/ is what generate writes.node .github/scripts/changelog.mjs check—changelog: 4 fragment(s) in changelog.d/ are well formed.npm run verify— exit 1 on this box with# tests 3147 / # pass 3145 / # fail 2, and both failures reproduce on the pristine base in the same environment:core/test/bench-preflight.test.jsrunsbench.shwith a PATH the test builds itself (mkdir: not found), andcore/test/thinking-row.test.jsmeasures a folded row in the npmjsdomthis box has rather than the harness's. Neither touchestoken-report; CI's harness supplies both.Checkpoint
{ "pr": 2781, "claim": "Dev-3", "issue": 2778, "summary": "token-report's default --db pointed one directory above the ledger: $DSH_HOME/usage.sqlite while the host writes $DSH_HOME/bot/usage.sqlite (core/index.js:94), so the tool exited 1 on every real install. The default now adds the bot segment, --db still wins and the missing-file sentence is unchanged, and the header comment plus docs/subsystems/ds-bot-core.md name the same path. A new test covers the default and the explicit override: red 1 fail / 1 pass at the base, 2/2 green with the fix, existing suite 5/5. The patch mirror for the two dev files is regenerated and canonical.", "files": ["core/dev/token-report.mjs", "core/test/token-report-default-db.test.js", "docs/subsystems/ds-bot-core.md", "changelog.d/2778-token-report-default-db.fixed.md", "patches/ds-bot/files/dev/token-report.mjs", "patches/ds-bot/files/test/token-report-default-db.test.js"], "checks": { "ran": "test/token-report-default-db.test.js (2/2, twice), test/token-report.test.js (5/5), ds-bot-patches check+canonical, changelog check, npm run verify (exit 1: 3145 pass / 2 fail, environment-only, reproduced on the pristine base)", "result": "green", "at": "f5f4bfeac5c396e0e9d76f36369add098022f3da" }, "next": "none — ready for review" }